#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/regex.hpp>
#include <boost/thread.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/thread.hpp>
#include <cstdlib>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <list>
#include <mutex>
#include <algorithm>
#include <boost/filesystem/operations.hpp>
#include <boost/iostreams/operations.hpp>
#include <boost/array.hpp>
#include <boost/algorithm/string.hpp>

using namespace std;
using boost::asio::ip::tcp;

//Global variables
string workingDirectoryLocation;
string serverHost;
string serverPort;
string inpTemplateFileLocation;
string configFileLocation = "client.cfg";
string calpuffLocation;
int numberOfThreads;

string getFileFromTcpStream(string fileLocation, boost::asio::ip::tcp::iostream &tcpStream)
{
	string lineFromStream;
	getline(tcpStream, lineFromStream); //pobieranie nazwy pliku
	if(tcpStream.bad())
	{
		perror("[Error]1 Blad odczytu nazwy pliku");
	}
	boost::filesystem::path filePath(fileLocation);
	if(boost::filesystem::is_directory(filePath))
	{
		filePath /= lineFromStream;
	}
	if(! boost::filesystem::exists(filePath.parent_path()))
	{
		boost::filesystem::create_directories(filePath.parent_path());
	}
	boost::filesystem::ofstream fileStream(filePath);
	if(! fileStream.is_open())
	{
		perror(("[Error]2 " + filePath.string()).c_str());
	}
	getline(tcpStream, lineFromStream); //pobieranie rozmiaru pliku
	int fileSize;
	try
	{
		fileSize = boost::lexical_cast<int>(lineFromStream);
	}
	catch(boost::bad_lexical_cast error)
	{
		cout << "bad_lexical_cast<int>: " << lineFromStream << "podczas pobierania: " << filePath.string();
		return "";
	}
	if(tcpStream.bad())
	{
		perror("[Error]3 Blad odczytu rozmiaru pliku");
	}
	int bufferSize = 2097152; //2MB
	char buffer[bufferSize];
	int sizeToRead = 0;
	while(1)
	{
		if(fileSize > bufferSize)
		{
			sizeToRead = bufferSize;
		}
		else
		{
			sizeToRead = fileSize;
		}
		tcpStream.read(buffer, sizeToRead);

		if(tcpStream.bad())
		{
			perror("[Error]4 Blad odczytu pliku z serwera");
		}
		fileStream.write(buffer, sizeToRead);
		if(fileStream.bad())
		{
			perror(("[Error]5 Blad zapisu do pliku " + filePath.string()).c_str());
		}
		fileStream.flush();
		fileSize -= sizeToRead;
		if(fileSize == 0)
		{//jezeli caly plik został odczytany, przerwij petle
			break;
		}
	}
	fileStream.close();
	//potwierdzenie odczytania całego pliku
	tcpStream << "OK\n";
	tcpStream << flush;
	return filePath.string();
}

bool sendFileWithTcpStream(string fileLocation, boost::asio::ip::tcp::iostream &tcpStream)
{
	boost::filesystem::path filePath(fileLocation);
	streamsize fileSize(boost::filesystem::file_size(filePath));
	boost::filesystem::ifstream fileStream(filePath, std::ios::in | std::ios::binary);
	if(! fileStream.is_open())
	{
		perror("[Error]6 ");
		fileStream.close();
		return false;
	}
	tcpStream << filePath.filename().string() << "\n";
	tcpStream << fileSize << "\n";
	boost::array<char, 1024> buffer;
	boost::system::error_code error;
	for(;;)
	{
		if(fileStream.eof() == false)
		{
			fileStream.read(buffer.c_array(), (std::streamsize)buffer.size());
			if(fileStream.gcount() <= 0)
			{
				cout << "[Error]7 Blad odczytu " << fileLocation << "\n";
				fileStream.close();
				return false;
			}
			tcpStream.write(buffer.c_array(), fileStream.gcount());
			if(error)
			{
				cout << "[Error]8 Blad wysyłania: " << error.message() << std::endl;
				fileStream.close();
				return false;
			}
		}
		else
		{
			break;
		}
	}
	fileStream.close();
	tcpStream << flush;
	//przed kontynuacja serwer czeka na potwierdzenie od klienta
	string tmp;
	tcpStream >> tmp;
	return true;
}

void RunProgram(boost::filesystem::path programToRunPath, boost::filesystem::path inpFilePath)
{
	string command = programToRunPath.string() + " ";
	command += boost::filesystem::complete(inpFilePath).string();
	command += " >Calpuff.log 2>&1";
	system(("cd " + inpFilePath.parent_path().string() + "\n" + command).c_str());
}

void CoreControler()
{
	string lineFromStream;
	//kontroler pętli
	bool exit = false;
	while(! exit)
	{
		try
		{
			tcp::iostream tcpStream(serverHost, serverPort);
			if(! tcpStream)
			{
				std::cout << "[Error]9 Nie mozna polaczyc sie z serwerem: " << tcpStream.error().message() << std::endl;
				exit = true;
				continue;
			}
			tcpStream << "get file\n";
			getline(tcpStream, lineFromStream);
			if(strcmp(lineFromStream.c_str(), "exit") == 0)
			{
				exit = true;
				continue;
			}
			else
			{
				string folderName = lineFromStream;
				boost::filesystem::path filePath(workingDirectoryLocation);
				filePath /= folderName;
				boost::system::error_code errorCode;
				boost::filesystem::create_directories(filePath, errorCode);
				if(errorCode)
				{
					printf("[Error]10 Blad podczas tworzenia folderu %s: %s\n", filePath.parent_path().c_str(), errorCode.message().c_str());
				}
				// pobieranie pliku z danymi
				getFileFromTcpStream(filePath.string(), tcpStream);
				// pobieranie CALPUFF.INP
				getFileFromTcpStream(filePath.string(), tcpStream);
				tcpStream >> lineFromStream;
				string numberOfSources = lineFromStream;
				tcpStream.close();
				//wstawianie liczby żródeł do pliku INP
				boost::regex stringToBeReplaced("! NAR2 =.*?!");
				string replacement("! NAR2 =" + numberOfSources + " !");
				filePath /= "CALPUFF.INP";
				boost::filesystem::fstream inpFileStream(filePath);
				if(inpFileStream)
				{
					std::string fileContent((std::istreambuf_iterator<char>(inpFileStream)), std::istreambuf_iterator<char>());
					std::string changedFileContent(boost::regex_replace(fileContent, stringToBeReplaced, replacement));
					inpFileStream.close();
					//otwieram ponownie plik z parametrami aby nadpisać jego zawartość
					inpFileStream.open(filePath, std::fstream::out | std::fstream::trunc);
					if(inpFileStream.is_open())
					{
						inpFileStream << changedFileContent;
						inpFileStream << flush;
						inpFileStream.close();
					}
				}
				boost::filesystem::path calpuffPath(calpuffLocation);
				boost::thread calpuffRunner = boost::thread(RunProgram, calpuffPath, filePath);
				calpuffRunner.join();
				tcp::iostream tcpStream(serverHost, serverPort);
				if(! tcpStream)
				{
					cout << "[Error]11 Nie mozna sie polaczyc z serwerem: " << tcpStream.error().message() << std::endl;
					exit = true;
					continue;
				}
				tcpStream << "completed: " << folderName << "\n";
				sendFileWithTcpStream((filePath.parent_path() / "CONC.DAT").string(), tcpStream);
				sendFileWithTcpStream((filePath.parent_path() / "Calpuff.log").string(), tcpStream);
				tcpStream >> lineFromStream;
				boost::filesystem::remove_all(filePath.parent_path(), errorCode);
				if(errorCode)
				{
					cout << "[Warning] wystąpił problem w czasie usuwania plików z folderu " << filePath.parent_path().string() << " : " << errorCode.message() << endl;
				}
				if(strcmp(lineFromStream.c_str(), "exit") == 0)
				{
					exit = true;
				}
				tcpStream.close();
			}
		}
		catch(std::exception& e)
		{
			cout << "Wystapil wyjatek: " << e.what() << std::endl;
			return;
		}
	}
}

bool ParseConfigFile(string configFileLocation)
{
	boost::filesystem::path configFilePath(configFileLocation);
	boost::filesystem::ifstream configFileStream(configFilePath);
	if(! configFileStream.is_open())
	{
		perror(("[Error]12 " + configFileLocation).c_str());
		return false;
	}
	string configLine;
	boost::regex commentLineRegex("^#.*"); //wyłapuje linie zaczynające się znakiem #
	boost::regex validLineRegex("^\\s*(\\w+)\\s*=(.*)$"); //wyłapuje linie typu zmienna=wartość z ewentualnymi spacjami
	boost::smatch regexResults;
	while(getline(configFileStream, configLine))
	{
		if(configFileStream.bad())
		{
			perror(("[Error]13 " + configFileLocation).c_str());
			return false;
		}

		if(! boost::regex_match(configLine, regexResults, commentLineRegex))
		{//wczytana linia nie jest komentarzem
			if(boost::regex_match(configLine, regexResults, validLineRegex))
			{//i zawiera przypisanie wartości do zmiennej
				//regexResults[1] - nazwa zmiennej, regexResults[2] - wartosc
				string varName(regexResults[1]);
				if(strcmp(varName.c_str(), "tmpFolder") == 0)
				{
					workingDirectoryLocation = regexResults[2];
					boost::filesystem::path testPath(workingDirectoryLocation);
					if(! boost::filesystem::exists(testPath))
					{
						boost::filesystem::create_directories(testPath);
					}
				}
				else if(strcmp(varName.c_str(), "serverHost") == 0)
				{
					serverHost = regexResults[2];
				}
				else if(strcmp(varName.c_str(), "serverPort") == 0)
				{
					try
					{
						string portNumberString = regexResults[2];
						boost::trim(portNumberString);
						int portNumber = boost::lexical_cast<int>(portNumberString);
						if(portNumber < 1024 || portNumber > 65535)
						{
							cout << "[Error]14 " << configFileLocation << ": Wartosc podana dla serverPort \"" << regexResults[2] << "\" jest nieprawidlowa. Wartosc powinna byc z zakresu 1024-65535.\n";
							return false;
						}
						serverPort = portNumberString;
					}
					catch(boost::bad_lexical_cast err)
					{
						cout << "[Error]15 " << configFileLocation << ": Wartosc podana dla serverPort \"" << regexResults[2] << "\" nie jest liczba\n";
						return false;
					}
				}
				else if(strcmp(varName.c_str(), "numberOfThreads") == 0)
				{
					try
					{
						numberOfThreads = boost::lexical_cast<int>(regexResults[2]);
					}
					catch(boost::bad_lexical_cast err)
					{
						cout << "[Error]16 " << configFileLocation << ": Wartosc podana dla numberOfThreads \"" << regexResults[2] << "\" nie jest liczba\n";
						return false;
					}
				}
				else if(strcmp(varName.c_str(), "programLocation") == 0)
				{
					calpuffLocation = regexResults[2];
				}
			}
		}
	}
	return true;
}

int main()
{
	//pobiera ilość wszystkich dostępnych rdzeni procesorów
	numberOfThreads = boost::thread::hardware_concurrency();

	//interpretacja pliku konfiguracyjnego
	if(! ParseConfigFile(configFileLocation))
	{
		printf("[Error]17 Blad podczas interpretacji pliku: %s", configFileLocation.c_str());
	}

	boost::filesystem::path calpuffPath(calpuffLocation);

	if(boost::filesystem::exists(calpuffPath))
	{
		//plik calpuff istnieje, porównaj wersję z tą na serwerze(data ostatniej modyfikacji)
		time_t localCalpuffVersion = boost::filesystem::last_write_time(calpuffPath);
		string serverCalpuffVersion;
		tcp::iostream tcpStream(serverHost, serverPort);
		if(tcpStream)
		{
			tcpStream << "get calpuff version\n";
			tcpStream >> serverCalpuffVersion;
			time_t LastModifiedTime;
			try
			{
				LastModifiedTime = boost::lexical_cast<time_t>(serverCalpuffVersion);
			}
			catch(boost::bad_lexical_cast error)
			{
				cout << "bad_lexical_cast<int>: " << serverCalpuffVersion << "podczas pobierania: wersji calpuffa z serwera";
			}
			if(localCalpuffVersion < LastModifiedTime)
			{//if file is older than on server then replacew
				tcpStream << "get calpuff\n";
				getFileFromTcpStream(calpuffPath.string(), tcpStream);
				tcpStream.close();
			}
		}
		else
		{
			std::cout << "[Error]18 Nie można polaczyc sie z serwerem: " << tcpStream.error().message() << std::endl;
		}
	}
	else
	{//calpuff nie istnieje, pobieramy go z serwera
		tcp::iostream tcpStream(serverHost, serverPort);
		if(! tcpStream)
		{
			std::cout << "[Error]19 Nie mozna polaczyc sie z serwerem: " << tcpStream.error().message() << std::endl;
			return 5;
		}
		tcpStream << "get calpuff\n";
		getFileFromTcpStream(calpuffPath.string(), tcpStream);
		tcpStream.close();
		boost::filesystem::permissions(calpuffPath, boost::filesystem::perms::all_all); //nadaje prawa do uruchamiania
	}

	printf("Uruchomiono %d watkow.\n", numberOfThreads);

	list<boost::thread> threads;
	for(int i = 0; i < numberOfThreads; i ++)
	{
		try
		{
			//Każdy wątek będzie wykonywał funkcję CoreControler
			threads.push_back(boost::thread(CoreControler));
		}
		catch(system_error err)
		{
			error_code errorCode = err.code();
			printf("[ERROR]20 Nie mozna było utworzyc watkow. #%d %s\n", errorCode.value(), errorCode.message().c_str());
			i --;
			continue;
		}
	}
	for(list<boost::thread>::iterator listIterator = threads.begin(); listIterator != threads.end(); ++ listIterator)
	{
		// Czekaj na zakończenie wszystkich wątków.
		listIterator->join();
	}
	printf("Operacje zakonczone.\n");
	return 0;
}
